import { DayOfWeekNum, TimePoint } from "./time-type";

/**
 * 将时间戳转成Date 对象
 * @param timestamp 毫秒表示的时间戳
 * @returns Date 对象
 */
function convertNumber2Date(timestamp: number): Date {
    if (timestamp < 1_000_000_000) {
        timestamp = 0;
    } else if (timestamp < 1_0_000_000_000) {
        timestamp *= 1000;
    }

    return new Date(timestamp);
}

// 将调度规则分为两部分：开头（Head）为固定时间点、结尾（Tail）为所在时间范围

abstract class Head {
    nextRule?: Tail;

    /**
     * 设置 Head 对象连接的 Tail
     * @param rule 可以连接到 Head 对象后面的 Tail 对象
     */
    setNextRule(rule: Tail) {
        this.nextRule = rule;
    }

    /**
     * 生成下一个时间，用 Date 对象表示，保证返回的 Date 对象表示的时间点在当前时间之后，在 leftBoudar 之后
     * @param leftBoundary 上一个时间点，或时间左界，如果类型为 number，则为毫秒表示的时间戳
     */
    abstract generateNextTime(leftBoundary: number | Date): Date;
}

interface Tail {
    /**
     * 调整 Date 对象表示的时间，使其落在合适的范围内
     * @param dt 被调整的 Date 对象
     * @param keepPoint 是否保持时间点（时分秒）不变
     */
    adjustTime(dt: Date, keepPoint: boolean): Date;
}

/**
 * 固定时间点规则，例如每天的 21:30，周六的 22:00
 */
class TimePointRule extends Head {
    private points: TimePoint[] = [];

    constructor(points: TimePoint | TimePoint[]) {
        super();
        if (Array.isArray(points)) {
            this.addPoints(points);
        } else {
            this.points.push(points);
        }
    }

    addPoint(point: TimePoint) {
        this.points.push(point);
        this.points.sort(TimePoint.compare);
        return this;
    }

    addPoints(points: TimePoint[]) {
        this.points.splice(this.points.length, 0, ...points);
        this.points.sort(TimePoint.compare);
        return this;
    }

    /**
     * 根据上一个时间点计算出下一个合法的时间点
     * @param leftBoundary 上一个时间点
     * @returns 下一个合法的时间点，下一个时间点和上一个时间点可能时分秒相同但个属于不同的天，也可能属于同一天，但是时分秒不同
     */
    generateNextTime(leftBoundary: number | Date): Date {
        if (!(typeof leftBoundary === "number")) {
            leftBoundary = leftBoundary.getTime();
        }

        if (leftBoundary < 1_000_000_000) {
            leftBoundary = 0;
        } else if (leftBoundary < 10_000_000_000) {
            leftBoundary *= 1000;
        }

        let nextDate: Date;
        if (leftBoundary < Date.now()) {
            nextDate = new Date();
        } else {
            nextDate = new Date(leftBoundary);
        }

        let nextPoint: TimePoint | undefined;
        for (const tp of this.points) {
            nextDate.setHours(tp.hour, tp.minute, tp.second, 0);
            if (leftBoundary < nextDate.getTime()) {
                nextPoint = tp;
                break;
            }
        }

        // 如果没找到下一个 nextPoint，则切换到后一天
        if (nextPoint === undefined) {
            nextDate.setDate(nextDate.getDate() + 1);
            nextDate.setHours(
                this.points[0].hour,
                this.points[0].minute,
                this.points[0].second,
                0
            );
        } else {
            nextDate.setHours(
                nextPoint.hour,
                nextPoint.minute,
                nextPoint.second
            );
        }

        if (this.nextRule != undefined) {
            return this.nextRule.adjustTime(nextDate, true);
        } else {
            return nextDate;
        }
    }
}

/**
 * 天级时间范围规则，例如周一至周五，周六，周天
 */
class DayRangeRule implements Tail {
    /**
     * 包含 7 个 boolean 值的数组，包含某天则对应的列表元素为 true，不包括则为 false
     */
    private days: boolean[];

    /**
     * @param _default 一周7天，每天是否默认，true 表示包括，false 表示不包括
     */
    constructor(_default: boolean) {
        this.days = Array.from({ length: 7 }, () => _default);
    }

    addDays(...days: DayOfWeekNum[]) {
        for (let day of days) {
            this.days[day] = true;
        }
    }

    excludeDays(...days: DayOfWeekNum[]) {
        for (let day of days) {
            this.days[day] = false;
        }
    }

    /**
     * 如果需要，调整时间对象 dt，使其落在本规则规定的天内
     * @param dt 需要被调整的时间
     * @param keepPoint 是否保持之前的时分秒，如果 true，则调整过程不会改变时分秒，只会改变天
     * @returns 被调整后的时间，可能等于被调整的时间
     */
    adjustTime(dt: Date, keepPoint: boolean = false): Date {
        let day = dt.getDay();
        if (this.days[day] === true) {
            return dt;
        }

        // 找到第一个为true的天
        for (let i = 1; i < 7; i++) {
            if (this.days[(day + i) & 7] === true) {
                dt.setDate(dt.getDate() + i);
                if (!keepPoint) {
                    dt.setHours(0, 0, 0, 0);
                }
                break;
            }
        }

        return dt;
    }
}

/**
 * 小时级时间范围规则，分为所在范围，和排除范围
 */
class HourRangeRule implements Tail {
    private includeOrExclude: boolean; // 包括或排除

    // 7 个元素的嵌套列表，每个列表元素都是时间段列表，同一个时间段列表中的时间段同属一天
    private rangeArrays: Array<Array<[TimePoint, TimePoint]>>;

    constructor(includeOrExclude: boolean) {
        this.includeOrExclude = includeOrExclude;
        this.rangeArrays = Array.from({ length: 7 }, () => []);
    }

    /**
     * 添加时间段
     * @param range 时间段，由两个 TimePoint 组成
     * @param daysOfWeek 时间段所在天
     */
    addRange(range: [TimePoint, TimePoint], ...daysOfWeek: DayOfWeekNum[]) {
        let days: DayOfWeekNum[];
        if (daysOfWeek.length == 0) {
            days = Array.from({ length: 7 }, (_, i) => i) as DayOfWeekNum[];
        } else {
            days = daysOfWeek;
        }

        for (const d of days) {
            this._addRangeToOneDay(range, d);
        }
    }

    /**
     * 将时间段添加到同属于某天(DayOfWeekNum)的列表
     * @param range 时间段，由两个 TimePoint 组成
     * @param day 时间段所在的天
     */
    private _addRangeToOneDay(
        range: [TimePoint, TimePoint],
        day: DayOfWeekNum
    ) {
        let ranges = this.rangeArrays[day];
        let resultRanges = [];
        let newRange = range;
        let i = 0;
        // 把 ranges 前面和 newRange 区间没重合（没有交集，且距离大于1）的区间加入到结果集
        while (
            i < ranges.length &&
            TimePoint.subtract(newRange[0], ranges[i][1]) > 1
        ) {
            resultRanges.push(ranges[i]);
            i++;
        }

        // 合并重合区间
        while (
            i < ranges.length &&
            TimePoint.subtract(ranges[i][0], newRange[1]) < 2
        ) {
            newRange[0] =
                TimePoint.subtract(ranges[i][0], newRange[0]) < 0
                    ? ranges[i][0]
                    : newRange[0];
            newRange[1] =
                TimePoint.subtract(ranges[i][1], newRange[1]) > 0
                    ? ranges[i][1]
                    : newRange[1];
            i++;
        }

        resultRanges.push(newRange);

        // 将剩余区间加入到结果集
        while (i < ranges.length) {
            resultRanges.push(ranges[i]);
            i++;
        }

        this.rangeArrays[day] = resultRanges;
    }

    /**
     * 调整时间 dt，使其落在本规则规定的时间范围内
     * @param dt 被调整的时间
     * @returns 调整后的时间
     */
    adjustTime(dt: Date): Date {
        let newDt = new Date(dt.getTime());

        let d = dt.getDay();
        let tp = new TimePoint(dt.getHours(), dt.getMinutes(), dt.getSeconds());

        if (this.includeOrExclude) {
            // 先在当天允许的时间段内检查，如果可以的话就在当天允许的时间段内调整
            let tps: [TimePoint, TimePoint][] = this.rangeArrays[d];
            let i = 0;
            while (i < tps.length) {
                if (TimePoint.compare(tp, tps[i][0]) < 0) {
                    newDt.setHours(
                        tps[i][0].hour,
                        tps[i][0].minute,
                        tps[i][0].second
                    );
                    break;
                } else if (TimePoint.compare(tp, tps[i][1]) <= 0) {
                    break;
                }

                i++;
            }

            // 如果遍历完了当天的时间段都没调整好，就得往后找第一个时间段不为空的天
            if (i == tps.length) {
                let j = 1;
                while (j < 8) {
                    let newD = (d + j) % 7;
                    if (this.rangeArrays[newD].length > 0) {
                        newDt.setDate(newDt.getDate() + j);
                        let _range = this.rangeArrays[newD][0][0];
                        newDt.setHours(
                            _range.hour,
                            _range.minute,
                            _range.second
                        );
                        break;
                    }
                }
            }
            return newDt;
        } else {
            while (true) {
                let i = 0;
                while (
                    i < this.rangeArrays[d].length &&
                    TimePoint.compare(tp, this.rangeArrays[d][i][1]) > 0
                ) {
                    i++;
                }
                if (i == this.rangeArrays[d].length) {
                    break;
                }

                if (TimePoint.compare(tp, this.rangeArrays[d][i][0]) < 0) {
                    break;
                } else {
                    newDt.setHours(
                        this.rangeArrays[d][i][1].hour,
                        this.rangeArrays[d][i][1].minute,
                        this.rangeArrays[d][i][1].second + 1
                    );
                }

                if (newDt.getDay() == d) {
                    break;
                } else {
                    d = newDt.getDay();
                }
            }
        }

        return newDt;
    }
}

/**
 * 频率规则，例如没多少天，多少小时，多少分钟，多少秒
 */
class Frequency extends Head {
    count: number; // 多少个时间单位
    unitDuration: number; // 单位时长，秒

    /**
     *
     * @param count 多少个时间单位
     * @param unitDuration 每个时间单位的时长（秒数）
     */
    constructor(count: number, unitDuration: number) {
        super();
        this.count = count;
        this.unitDuration = unitDuration;
    }

    /**
     * 根据上一个时间、本规则规定频率以及当前时间产生下一个时间，但是保证产生的时间为未来时间
     * @param lastTime 上一个时间
     * @returns 根据上一个时间和本频率规则计算出的下一个时间
     */
    generateNextTime(lastTime: number | Date): Date {
        if (typeof lastTime === "number") {
            if (lastTime < 1_000_000_000) {
                lastTime = 0;
            } else if (lastTime < 10_000_000_000) {
                lastTime *= 1000;
            }
        } else {
            lastTime = lastTime.getTime();
        }

        let nextTime = lastTime + this.count * this.unitDuration * 1000;

        if (nextTime < Date.now()) {
            nextTime = Date.now() + 1000;
        }

        let newDate = new Date(nextTime);
        if (this.nextRule != undefined) {
            newDate = this.nextRule.adjustTime(newDate, false);
        }

        return newDate;
    }
}

class SecondFrequency extends Frequency {
    constructor(count: number) {
        super(count, 1);
    }
}

class MinuteFrequency extends Frequency {
    constructor(count: number = 1) {
        super(count, 60);
    }
}

class HourFrequency extends Frequency {
    constructor(count: number = 1) {
        super(count, 3600);
    }
}

class DayFrequency extends Frequency {
    constructor(count: number = 1) {
        super(count, 86400);
    }
}

/**
 * 规则链
 * 内部至少包含一个 HeadRule 对象，该 HeadRule 可选的连接一个 TailRule 对象。
 * 未来随着规则越来越复杂，一个规则链内部可以包含三个及以上规则对象。
 */
class RuleChain {
    private headRule: Head;

    constructor(headRule: Head, tailRule?: Tail) {
        this.headRule = headRule;
        if (tailRule != undefined) {
            this.headRule.setNextRule(tailRule);
        }
    }

    setTailRule(rule: Tail) {
        this.headRule.setNextRule(rule);
    }

    /**
     *
     * @param previousTime 上个时间，毫秒表示的时间戳
     * @returns 下一个合法时间点，毫秒表示的时间戳
     */
    generateNextTime(previousTime: number): number {
        return this.headRule.generateNextTime(previousTime).getTime() as number;
    }
}

export {
    Head,
    Tail,
    RuleChain,
    TimePointRule,
    Frequency,
    SecondFrequency,
    MinuteFrequency,
    HourFrequency,
    DayFrequency,
    HourRangeRule,
    DayRangeRule,
};
